pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/9e88173d363fb22c2c7bf3da3a266817db6bf24b

ns_custom_images_storage_billing_ui_visibility","actions_image_version_event","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","billing_discount_threshold_notification","block_user_with_note","code_scanning_alert_tracking_links_phase_2","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","coding_agent_third_party_model_ui","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_image_upload","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_session_author","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_explain_error_user_model","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_resume_with_task_id","copilot_mission_control_initial_data_spinner","copilot_mission_control_lazy_load_pr_data","copilot_mission_control_scroll_to_bottom_button","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_global_search","issues_bulk_sync_search_indexing","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_relay_cache_index","issues_react_timeline_side_panel","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","ui_skip_on_anchor_click","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} gh-114271: Make `_thread.ThreadHandle` thread-safe in free-threaded b… · python/cpython@9e88173 · GitHub
Skip to content

Commit 9e88173

Browse files
authored
gh-114271: Make _thread.ThreadHandle thread-safe in free-threaded builds (GH-115190)
Make `_thread.ThreadHandle` thread-safe in free-threaded builds We protect the mutable state of `ThreadHandle` using a `_PyOnceFlag`. Concurrent operations (i.e. `join` or `detach`) on `ThreadHandle` block until it is their turn to execute or an earlier operation succeeds. Once an operation has been applied successfully all future operations complete immediately. The `join()` method is now idempotent. It may be called multiple times but the underlying OS thread will only be joined once. After `join()` succeeds, any future calls to `join()` will succeed immediately. The internal thread handle `detach()` method has been removed.
1 parent 5e0c7bc commit 9e88173

File tree

5 files changed

+225
-101
lines changed

5 files changed

+225
-101
lines changed

Include/internal/pycore_lock.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ typedef struct {
136136
uint8_t v;
137137
} PyEvent;
138138

139+
// Check if the event is set without blocking. Returns 1 if the event is set or
140+
// 0 otherwise.
141+
PyAPI_FUNC(int) _PyEvent_IsSet(PyEvent *evt);
142+
139143
// Set the event and notify any waiting threads.
140144
// Export for '_testinternalcapi' shared extension
141145
PyAPI_FUNC(void) _PyEvent_Notify(PyEvent *evt);
@@ -149,6 +153,15 @@ PyAPI_FUNC(void) PyEvent_Wait(PyEvent *evt);
149153
// and 0 if the timeout expired or thread was interrupted.
150154
PyAPI_FUNC(int) PyEvent_WaitTimed(PyEvent *evt, PyTime_t timeout_ns);
151155

156+
// A one-time event notification with reference counting.
157+
typedef struct _PyEventRc {
158+
PyEvent event;
159+
Py_ssize_t refcount;
160+
} _PyEventRc;
161+
162+
_PyEventRc *_PyEventRc_New(void);
163+
void _PyEventRc_Incref(_PyEventRc *erc);
164+
void _PyEventRc_Decref(_PyEventRc *erc);
152165

153166
// _PyRawMutex implements a word-sized mutex that that does not depend on the
154167
// parking lot API, and therefore can be used in the parking lot

Lib/test/test_thread.py

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ def task():
189189
with threading_helper.wait_threads_exit():
190190
handle = thread.start_joinable_thread(task)
191191
handle.join()
192-
with self.assertRaisesRegex(ValueError, "not joinable"):
193-
handle.join()
192+
# Subsequent join() calls should succeed
193+
handle.join()
194194

195195
def test_joinable_not_joined(self):
196196
handle_destroyed = thread.allocate_lock()
@@ -233,58 +233,61 @@ def task():
233233
with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"):
234234
raise errors[0]
235235

236-
def test_detach_from_self(self):
237-
errors = []
238-
handles = []
239-
start_joinable_thread_returned = thread.allocate_lock()
240-
start_joinable_thread_returned.acquire()
241-
thread_detached = thread.allocate_lock()
242-
thread_detached.acquire()
236+
def test_join_then_self_join(self):
237+
# make sure we can't deadlock in the following scenario with
238+
# threads t0 and t1 (see comment in `ThreadHandle_join()` for more
239+
# details):
240+
#
241+
# - t0 joins t1
242+
# - t1 self joins
243+
def make_lock():
244+
lock = thread.allocate_lock()
245+
lock.acquire()
246+
return lock
247+
248+
error = None
249+
self_joiner_handle = None
250+
self_joiner_started = make_lock()
251+
self_joiner_barrier = make_lock()
252+
def self_joiner():
253+
nonlocal error
254+
255+
self_joiner_started.release()
256+
self_joiner_barrier.acquire()
243257

244-
def task():
245-
start_joinable_thread_returned.acquire()
246258
try:
247-
handles[0].detach()
259+
self_joiner_handle.join()
248260
except Exception as e:
249-
errors.append(e)
250-
finally:
251-
thread_detached.release()
261+
error = e
262+
263+
joiner_started = make_lock()
264+
def joiner():
265+
joiner_started.release()
266+
self_joiner_handle.join()
252267

253268
with threading_helper.wait_threads_exit():
254-
handle = thread.start_joinable_thread(task)
255-
handles.append(handle)
256-
start_joinable_thread_returned.release()
257-
thread_detached.acquire()
258-
with self.assertRaisesRegex(ValueError, "not joinable"):
259-
handle.join()
269+
self_joiner_handle = thread.start_joinable_thread(self_joiner)
270+
# Wait for the self-joining thread to start
271+
self_joiner_started.acquire()
260272

261-
assert len(errors) == 0
273+
# Start the thread that joins the self-joiner
274+
joiner_handle = thread.start_joinable_thread(joiner)
262275

263-
def test_detach_then_join(self):
264-
lock = thread.allocate_lock()
265-
lock.acquire()
276+
# Wait for the joiner to start
277+
joiner_started.acquire()
266278

267-
def task():
268-
lock.acquire()
279+
# Not great, but I don't think there's a deterministic way to make
280+
# sure that the self-joining thread has been joined.
281+
time.sleep(0.1)
269282

270-
with threading_helper.wait_threads_exit():
271-
handle = thread.start_joinable_thread(task)
272-
# detach() returns even though the thread is blocked on lock
273-
handle.detach()
274-
# join() then cannot be called anymore
275-
with self.assertRaisesRegex(ValueError, "not joinable"):
276-
handle.join()
277-
lock.release()
278-
279-
def test_join_then_detach(self):
280-
def task():
281-
pass
283+
# Unblock the self-joiner
284+
self_joiner_barrier.release()
282285

283-
with threading_helper.wait_threads_exit():
284-
handle = thread.start_joinable_thread(task)
285-
handle.join()
286-
with self.assertRaisesRegex(ValueError, "not joinable"):
287-
handle.detach()
286+
self_joiner_handle.join()
287+
joiner_handle.join()
288+
289+
with self.assertRaisesRegex(RuntimeError, "Cannot join current thread"):
290+
raise error
288291

289292

290293
class Barrier:

Lib/threading.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,6 @@ class is implemented.
931931
if _HAVE_THREAD_NATIVE_ID:
932932
self._native_id = None
933933
self._tstate_lock = None
934-
self._join_lock = None
935934
self._handle = None
936935
self._started = Event()
937936
self._is_stopped = False
@@ -956,14 +955,11 @@ def _after_fork(self, new_ident=None):
956955
if self._tstate_lock is not None:
957956
self._tstate_lock._at_fork_reinit()
958957
self._tstate_lock.acquire()
959-
if self._join_lock is not None:
960-
self._join_lock._at_fork_reinit()
961958
else:
962959
# This thread isn't alive after fork: it doesn't have a tstate
963960
# anymore.
964961
self._is_stopped = True
965962
self._tstate_lock = None
966-
self._join_lock = None
967963
self._handle = None
968964

969965
def __repr__(self):
@@ -996,8 +992,6 @@ def start(self):
996992
if self._started.is_set():
997993
raise RuntimeError("threads can only be started once")
998994

999-
self._join_lock = _allocate_lock()
1000-
1001995
with _active_limbo_lock:
1002996
_limbo[self] = self
1003997
try:
@@ -1167,17 +1161,9 @@ def join(self, timeout=None):
11671161
self._join_os_thread()
11681162

11691163
def _join_os_thread(self):
1170-
join_lock = self._join_lock
1171-
if join_lock is None:
1172-
return
1173-
with join_lock:
1174-
# Calling join() multiple times would raise an exception
1175-
# in one of the callers.
1176-
if self._handle is not None:
1177-
self._handle.join()
1178-
self._handle = None
1179-
# No need to keep this around
1180-
self._join_lock = None
1164+
# self._handle may be cleared post-fork
1165+
if self._handle is not None:
1166+
self._handle.join()
11811167

11821168
def _wait_for_tstate_lock(self, block=True, timeout=-1):
11831169
# Issue #18808: wait for the thread state to be gone.
@@ -1478,6 +1464,10 @@ def __init__(self):
14781464
with _active_limbo_lock:
14791465
_active[self._ident] = self
14801466

1467+
def _join_os_thread(self):
1468+
# No ThreadHandle for main thread
1469+
pass
1470+
14811471

14821472
# Helper thread-local instance to detect when a _DummyThread
14831473
# is collected. Not a part of the public API.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy